/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NJ = Ninject.Core.Infrastructure;
using Nvigorate.Common;
using Nvigorate.Common.Reflection;
using Nvigorate.Extensions;
using Nvigorate.Relational.Mapping;
using Nvigorate.Relational.Mapping.Index;
using Nvigorate.Relational.Mapping.Interface;
using Nvigorate.Relational.Record;
using Nvigorate.Relational.State;
using Nvigorate.Relational.Visitors;

namespace Nvigorate.Relational.Repository
{
    public class DataReaderBinder<T> : IResultVisitor
    {
        protected Dictionary<Type, List<RelativeDecorator>> _instanceDictionary = new Dictionary<Type, List<RelativeDecorator>>();
        protected Dictionary<Tuple<Type, Type, string>, List<object>> _relativeRegistry = new Dictionary<Tuple<Type, Type, string>, List<object>>();
        protected List<Type> _subjectsProcessed = new List<Type>();
        protected DynamicAccessorCache _accessor = new DynamicAccessorCache();

        protected IRepositoryMapIndex RepositoryMapIndex { get; set; }
        protected bool _relationshipsEstablished = false;
        protected static Tuple<Type, Type, string> _rootKey = Tuple.Create(typeof(object), typeof(T), "");

        public IEnumerable<T> Instances
        {
            get
            {
                if (!_relationshipsEstablished)
                {
                    Stitch();
                    _relationshipsEstablished = true;
                }
                _instanceDictionary[typeof(T)].Select(r => r.Instance).OfType<ITrackState>().ForEach(i => i.Deserializing = false);
                return _instanceDictionary[typeof(T)].Select(r => r.Instance).Cast<T>();
            }
        }

        public void Visit(IEnumerable<IRecord> records, ITargetRelationship relationship)
        {
            var type = relationship == null ? typeof (T) : relationship.RelativeType;
            var map = RepositoryMapIndex.GetMap(type);
            var binder = Binder.GetTypedBinder(type, map);
            var list = GetInstances(records, map, binder).ToList();
            List<RelativeDecorator> collection = null;
            _instanceDictionary.TryGetValue(type, out collection);
            if (collection == null)
            {
                _instanceDictionary[type] = list;
            }
            else
                collection.AddRange(list);
        }

        public IEnumerable<RelativeDecorator> GetInstances(IEnumerable<IRecord> records, IBindingMap map, Binder binder)
        {
            //var type = map.Discriminator != null ? map.Discriminator.GetAppropriateType(records) : map.TargetType;
            return binder.Bind(records).Select(i =>
                                            {
                                                var decorator = new RelativeDecorator(map, i);
                                                decorator.RelativeKeys.ForEach(k => Register(k.Value, decorator));
                                                return decorator;
                                            });
        }

        protected void Register(Tuple<Type, Type, string> key, RelativeDecorator instance)
        {
            List<object> list = null;
            _relativeRegistry.TryGetValue(key, out list);
            if(list == null)
                _relativeRegistry.Add(key, new List<object>() { instance.Instance });
            else
            {
                list.Add(instance.Instance);
            }
        }

        protected Type GetObjectType(PropertyInfo propertyInfo)
        {
            return Reflector.IsEnumerable(propertyInfo.PropertyType) ?
                                                                         Enumerable.First(propertyInfo.PropertyType.GetGenericArguments()) :
                                                                                                                                     propertyInfo.PropertyType;

        }

        protected void Stitch()
        {
            _instanceDictionary.ForEach(Stitch);
        }

        protected void Stitch(KeyValuePair<Type, List<RelativeDecorator>> instances)
        {
            var relatives = QueryBuilderHelper.GetRelatives(this.RepositoryMapIndex, instances.Key);
            foreach(var instance in instances.Value)
            {
                foreach(var relative in relatives)
                {
                    List<object> children = null;
                    _relativeRegistry.TryGetValue(instance.GetKey(relative), out children);
                    Reflector.Write(instance.Instance, relative.RelativeProperty, children ?? new List<object>());
                } 
            }
                
               
        }

        protected string GetParentGroupKey(object instance, ITargetRelationship relationship)
        {
            var oneToMany = relationship as OneToManyRelationship;
            var manyToMany = relationship as ManyToManyRelationship;

            return oneToMany != null
                       ?
                           DelimitedBuilder.Construct(
                               oneToMany.Constraints.Select(c => Reflector.Read(instance, c.Key).ToString()), "|")
                       :
                           DelimitedBuilder.Construct(
                               manyToMany.Container.SubjectPairs.Select(p => Reflector.Read(instance, p.Target).ToString()), "|");
        }

        protected string GetChildGroupKey(object instance, ITargetRelationship relationship)
        {
            var oneToMany = relationship as OneToManyRelationship;
            var manyToMany = relationship as ManyToManyRelationship;

            return oneToMany != null
                       ?
                           DelimitedBuilder.Construct(
                               oneToMany.Constraints.Select(c => Reflector.Read(instance, c.Value).ToString()), "|")
                       :
                           DelimitedBuilder.Construct(
                               manyToMany.Container.RelativePairs.Select(p => Reflector.Read(instance, p.Target).ToString()), "|");
        }

        protected IEnumerable<object> FilterRelatives(List<object> children, object parent, ITargetRelationship relationship)
        {
            var oneToMany = relationship as OneToManyRelationship;
            var manyToMany = relationship as ManyToManyRelationship;

            if(oneToMany != null)
            {
                return
                    Enumerable.Where(children, i =>
                        Enumerable.All(oneToMany.Constraints, p => Reflector.Read(i, p.Value).Equals(Reflector.Read(parent, p.Key))));
            }
            else
            {
                var constraints =
                    manyToMany.Container.SubjectPairs.Select(s => s.Target).ZipToDictionary(
                        manyToMany.Container.RelativePairs.Select(s => s.Target));
                return
                    Enumerable.Where(children, i =>
                        Enumerable.All(constraints, p => Reflector.Read(i, p.Value).Equals(Reflector.Read(parent, p.Key))));
            }
        }

        protected object PopulateChildProperties(object instance, IRecord record, List<Type> inheritenceStack)
        {
            var type = instance.GetType();
            inheritenceStack.Add(type);
            var properties = QueryBuilderHelper.GetRelatives(RepositoryMapIndex, type);
            //properties.ForEach(p => _accessor.Write(instance, p.RelativeProperty, GetChildPropertyValue(type, p.RelativeType, record, p.RelativeProperty, inheritenceStack)));
            return this;
        }

        protected void PostBinding(object instance, IBindingMap map)
        {
            var tracker = instance as ITrackState;
            if (tracker != null)
            {
                map.MarkPersistedSources(tracker.InstanceState);
                map.MarkTargetKeys(tracker.InstanceState);
                tracker.InstanceState.ClearDirtyState();
                tracker.Deserializing = false;
            }
        }

        public DataReaderBinder(IRepositoryMapIndex repositoryMapIndex)
        {
            RepositoryMapIndex = repositoryMapIndex;
            RelativeDecorator.Index = repositoryMapIndex;
        }
    }
}